<?php
/**
 * form.class
 *
 * script
 *
 * @version:	0.1a
 * @package:	global
 * @author:	alex_ez
 * @created:	21.05.2009 05:13:12
 *
 * @copyright:	(c) 2009 alex <qfox@ya.ru> yaroshevich aka alex_ez. All rights reserved.
 */

class FormException extends Exception
{
	protected $field = null;
	
	public function __construct( $message = '', $code = 0, Exception $previous = null, $field = null )
	{
		$this ->field = $field;
		$this ->previous = $previous;
		if( phpversion( ) > 5.2 )
			parent::__construct( $message, $code, $previous );
		else
			parent::__construct( $message, $code );
	}
	
	public function toArray( )
	{
		$message = str_replace(
				array( 'incorrect-captcha-sol' ),
				array( 'Введена с ошибкой' ),
				$this ->getMessage( )
				);
		return array(
			'error' => $message,
			'code' => $this ->getCode( ),
			'field' => $this ->field );
	}
	
	public function toJSON( )
	{
		return json::encode( $this ->toArray( ) );
	}
}

class Form
{
	private $owner = null;
	private $url = null;
	
	private $options = array( );
	private $lists = array( );
	
	public function __construct( Controller $c, $url = null, array $options = null )
	{
		$this ->owner = $c;
		$this ->url = (!is_null($url)) ? ($url) : $c->getInnerUrl();
		
		$this ->setOption( 'action', '/ajax'.$this ->url );
		$this ->setOption( 'method', 'POST' );
		$this ->setOption( 'name', str_replace( '/', '_', trim( $this->url, '/' ) ) );
		
		$this ->setOptions( $options );
		$this ->setData( array( ) );
	}
	
	public function setOption( $k, $v )
	{
		if( $k == 'mode' )
		{
			if( !is_string( $v ) ) return;
			$modes = array_map( 'trim', array_map( 'strtolower', explode(',',$v) ) );
			
			foreach( array_filter( $modes ) as $vk )
				$this ->options[ $k ][ ltrim( $vk, ' -!' ) ] = (strpos( '-!', $vk[0] ) === false);
		}
		else
			$this ->options[ $k ] = $v;
		
/*		if( @$this ->options['enctype'] && !defined( 'qqq' ) )
			define( 'qqq', 'true' );
		
		if( defined( 'qqq' ) )
			var_dump( $this ->options );//*/
	}
	
	public function setOptions( $a )
	{
		if( !is_array( $a ) ) return;
		
		foreach( $a as $k => $v )
			$this ->setOption( $k, $v );
	}
	
	public function getOptions( )
	{
		return json::encode( $this ->options );
	}

	// todo: add [] support
	public function getFormattedData( )
	{
		$formatted = array( );
		$data = $this ->getPostData( );
		$fields = $this ->getPostFields( );
		
		foreach( $fields as $k => $f )
		{
			if( $f['type'] == 'list' || $f['type'] == 'radio' )
			{
				$values = _a::flat( $f['values'], 'id', 'title' );
				$value = $values[$data[$k]];
			}
			else
				$value = $data[$k];
			
			$formatted[ $f['label'] ] = $value;
		}
		
		return $formatted;
	}
	
	public function setData( $data )
	{
		$this ->data = is_array( $data ) ? ( isset($data['data']) && is_string( $data['data'] ) ? json::decode( $data ) : $data ) : json::decode( $data );
		$this ->data = _a::simplify( $this ->data );
		//fb::log( $data, 'data before' );
		//fb::log( $this ->data, 'data after' );
	}
	
	public function setLists( $a, $template = null )
	{
		foreach( $a as $k => $v )
		{
			$this ->setList( $k, $v, $template );
		}
	}
	
	public function setList( $var, $list, $template = null, $deeper = false )
	{
		if( !is_array( $list ) ) throw new FormException( "List {$var} must be an array." );
		
		$this ->lists[ $var ] = array( );
		if( !count( $list ) ) return;

		$i = reset( $list );
		if( is_null( $template ) && is_array( $i ) )
		{
			foreach( array( 'title', 'value', 'name', 'tag', 'id' ) as $_key )
				if( array_key_exists( $_key, $i ) ) break;
			$template = "{{$_key}}";
		}
		
		$this ->lists[ $var ] = $this ->setList_operate( $list, $template, $deeper );
	}

	// handler
	private function setList_operate( $list, $template, $deeper = false )
	{
		$r = array( );
		
		foreach( $list as $k => $i )
		{
			// gg
			if( $deeper && is_array( $i ) && !isset( $i['id'] ) )
			{
				$r[$k] = $this ->setList_operate( $i, $template );
				continue;
			}
			
			// classic
			$id = is_array( $i ) && isset( $i['id'] ) ? $i['id'] : $k;
			
			$title = !is_array( $i )
				? $i
				: ( str_replace(
				array_map( create_function('$v','return "{{$v}}";'), array_keys( $i ) ),
				array_map( 'strval', array_values( $i ) ),
				(string)$template ) );
			
			$r[$k] = array( 'id' => $id, 'title' => $title );
		}
		
		return $r;
	}
	
	public function getList( $var )
	{
		if( !isset( $this->lists[$var] ) )
			$this ->setList( $var, explode(',',$var) );
		
		return $this->lists[$var];
	}
	
	// todo: add [] support
	public function getFieldSet( $url = null, $decode = false )
	{
		$url = !is_null( $url ) ? $url : $this ->url;
		$r = $this ->owner ->getFieldSet( $url, 1 );
		//fb::log( array_keys( $r ), 'f keys' );
		foreach( array_keys( $r ) as $fieldKey )
		{
			// set enctype if file
			if( $r[$fieldKey]['type'] == 'file' )
			{
				$this ->setOptions( array(
					'enctype' => 'multipart/form-data',
					'class' => 'ajax file',
					'file' => true,
					) );
			}
			
			// fill lists
			if( ( $r[$fieldKey]['type'] == 'list' || $r[$fieldKey]['type'] == 'radio' ) && isset( $r[$fieldKey]['values'] ) )
			{
				$r[$fieldKey]['values'] = $this ->getList( $r[$fieldKey]['values'] );
			}
			elseif( ($r[$fieldKey]['type'] == 'typable' || $r[$fieldKey]['type'] == 'typeable') && isset( $r[$fieldKey]['types'] ) )
			{
				$r[$fieldKey]['type'] = 'typeable';
				$r[$fieldKey]['types'] = $this ->getList( $r[$fieldKey]['types'] );
			}
			
			// if ends with brackets
			$multiple = ( substr($fieldKey,-2,2) == '[]' );
			$r[$fieldKey]['multiple'] = $multiple;


			// set defaults
			$_keys = array( );
			$_keys[] = $fieldKey;
			
			// typifying
			if( $r[$fieldKey]['type'] == 'typeable' )
			{
				$_tkeys = array( );
				foreach( array_keys( $r[$fieldKey]['types'] ) as $fieldType )
					$_tkeys[$fieldType] = str_replace( '[{type}]', "[{$fieldType}]", $fieldKey );
				$_keys = $_tkeys;
			}
			if( $r[$fieldKey]['type'] == 'money' && isset($r[$fieldKey]['value']) )
				$r[$fieldKey]['value'] = number_format( $r[$fieldKey]['value'], 2, '.', ',' );
			
			foreach( $_keys as $type => $key )
			{
				if( isset($r[$fieldKey]['const']) && $r[$fieldKey]['const'] )
					break;
				
				if( $r[$fieldKey]['multiple'] && strpos( $key, '[]' ) === false )
					$key .= '[]';

				$typeable = $r[$fieldKey]['type'] == 'typeable';
				for( $i = -1, $j = 5; $j; $i++ )
				{
					$_k = str_replace( '[]', $i == -1 ? '' : "[{$i}]", $key );
					if( !array_key_exists( $_k, $this->data ) ) { $j--; continue; }
					
					$value = $this ->data[$_k];
					switch( $r[$fieldKey]['type'] )
					{
					case 'typeable':	$value = array( 'type' => $type, 'value' => $value ); break;
					case 'money':		$value = number_format( $value, 2, '.', ',' ); break;
					}

					if( !isset( $r[$fieldKey]['value'] ) || !is_array( $r[$fieldKey]['value'] ) )
						$r[$fieldKey]['value'] = array( );
					
					$r[$fieldKey]['value'][] = $value;
					unset( $this ->data[$_k] );
				}
			}
		}
		//if( isset( $this ->data ) ) fb::log( $this ->data, 'default data' ); else fb::log( 'no default data' );
		
		return $decode ? $r : json::encode( $r );
	}
	
	private function getPostFields( )
	{
		$r = array( );
		$fs = $this ->getFieldSet( $this ->url, 1 );
		foreach( $fs as $k => $f )
		if( !strpos( ',header,static', $f['type'] ) )
		{
			$r[$k] = $f;
		}
		return $r;
	}

	public function getGetData( )
	{
		$__post = $_POST;
		$_POST = $_GET;
		$r = $this ->getPostData();
		$_POST = $__post;
		return $r;
	}

	public function getPostData( )
	{
		$fs = $this ->getPostFields( );
		
		$r = request::__getMe( );
		$_r = array( );
		
		$oneof = array( );
		//fb::log( $fs, 'fs' );
		//fb::log( $r ->f( array_keys( $_FILES ) ), 'request->files' );
		
		$posted = $r ->p( array_keys( $_POST ) );
		//fb::log( $posted, 'post' );
		$posted = _a::simplify( $posted );
		fb::log( $posted, 's post' );
		
		foreach( $fs as $fieldKey => $f )
		{
			$_keys = array( );
			$_keys[] = $fieldKey;
			
			// typifying
			if( $f['type'] == 'typeable' )
			{
				$_tkeys = array( );
				foreach( $_keys as $k )
				foreach( array( 'type', 'value' ) as $i )
					$_tkeys[] = $f['multiple']
						? str_replace( '[]', "[{$i}][]", $k )
						: $k."[{$i}]";
				$_keys = $_tkeys;
			}
			
			// multiplying
			if( $f['multiple'] )
			{
				$_mkeys = array( );
				foreach( $_keys as $k )
				{
					//fb::info( $f['label'], $k );
					$i = 0; $_key = str_replace( '[]', "[{$i}]", $k );
					for( ; array_key_exists( $_key, $posted ); $i++, $_key = str_replace( '[]', "[{$i}]", $k ) )
						$_mkeys[] = $_key;
				}
				$_keys = $_mkeys;
			}
			//fb::log( $_keys, $k.'_keys' );
			
			// fetch data by simplified keys
		foreach( $_keys as $k )
		{
			if( !isset($f['label'] ) ) $f['label'] = $k;
			
			if( $f['type'] == 'file' )
			{
				parse_str( $k, $kParsed );
				$k0 = key( $kParsed ); $k1 = null;
				
				$postKey = ( $f['type'] == 'file' ? 'file' : 'post' ).'_'.$k0;
				$v = $r->$postKey; // value
				
				if( is_array( reset( $kParsed ) ) )
				{
					$k1 = key( reset( $kParsed ) );
					$v = $v[$k1];
				}
			}
			else
				$v = isset( $posted[ $k ] ) ? $posted[ $k ] : null;
			//fb::log( $v, $k );
			
			// one of groupping
			if( isset( $f['oneof'] ) && $f['oneof'] && !isset($oneof[$f['oneof']]) )
				$oneof[$f['oneof']] = null;
			
			if( !isset($f['donttouch']) || !$f['donttouch'] )
			{
			// trimming if no spaces at the first
			if( $f['type'] != 'file' && !( isset($f['spaces']) && $f['spaces'] ) )
				$v = trim( $v );
			
			// require test at the second
			if( $f['type'] != 'file' && $f['type'] != 'captcha' && !isset($f['oneof']) && isset($f['required']) && $f['required'] && (!$v || !mb_strlen($v)) )
				throw new FormException( "Поле ".$f['label']." обязательно к заполнению.", 0, null, $k );
			
			// some parses
			if( $f['type'] == 'money' ) {
				$v = str_replace( ',', '', $v );
				if( (float)$v != 0 && (float)$v < .1 )	throw new FormException( "Поле ".$f['label']." заполнено неверно.", 0, null, $k );
			} elseif( $f['type'] == 'date' ) {
				$v = strtotime( $v );
				if( $v === false && $f['required'] )	throw new FormException( "Поле ".$f['label']." заполнено неверно. Разрешенный формат ММ.ДД.ГГГГ", 0, null, $k );
			} elseif( $f['type'] == 'month' ) {
				$v = strtotime( '01.'.$v );
				if( $v === false && $f['required'] )	throw new FormException( "Поле ".$f['label']." заполнено неверно. Разрешенный формат ММ.ГГГГ", 0, null, $k );
			} elseif( $f['type'] == 'phone' ) {
				$_v = preg_replace( '/[^0-9]/i', '', $v );
				if( strlen($v) && strlen($_v) < 6 )	throw new FormException( "Поле ".$f['label']." заполнено неверно.", 0, null, $k );
			} elseif( $f['type'] == 'number' ) {
				if( $v && !ctype_digit( ''.$v ) )	throw new FormException( "Поле ".$f['label']." заполнено неверно.", 0, null, $k );
			} elseif( $f['type'] == 'file' ) {
				if( isset($f['required']) && $f['required'] && $v['error'] == UPLOAD_ERR_NO_FILE )
					throw new FormException( "Файл ".$f['label']." обязателен.", 0, null, $k );
				if( $v['error'] && $v['error'] != UPLOAD_ERR_NO_FILE )
					throw new FormException( "Файл ".$f['label']." загружен с ошибкой.".json::encode($v), 0, null, $k );
				if( $v['error'] == UPLOAD_ERR_NO_FILE ) continue;
			} elseif( $f['type'] == 'captcha' ) {
				$f['challenge'] = trim( $r ->p_recaptcha_challenge_field );
				$f['response'] = trim( $r ->p_recaptcha_response_field );
				fb::log( $f, 'captcha field!' );
				if( !$f['response'] ) throw new FormException( "Поле ".$f['label']." обязательно к заполнению.", 0, null, $k );
				
				try { $f['value'] = recaptcha::checkAnswer( $f['challenge'], $f['response'] ); }
				catch( RecaptchaException $e ) { throw new FormException( $e ->getMessage(), 0, $e, $k ); }
				
				if( !$f['value'] ->is_valid ) throw new FormException( $f['value']->error, 0, null, $k );
			}
			}
			
			// fixedLength test
			if( isset($f['fixedLength']) && !(mb_strlen($v)==$f['fixedLength']||mb_strlen($v)==0) )
				throw new FormException( sprintf( "Длина поля %s должна быть ровно %d символов. (сейчас %d).", $f['label'], $f['fixedLength'], mb_strlen($v) ), 0, null, $k );
			// mask test
			if( isset($f['mask']) && (!preg_match('@^'.str_replace(array('9','X','-',' '),array('[0-9]','[a-z]'),$v).'$@i',$v)) )
				throw new FormException( sprintf( "Поле %s должно соответствовать маске %s.", $f['label'], $f['mask'] ), 0, null, $k );

			// sets
			// $k = 'zhopa[zyu][xep][pezda][chmo][ololo][eblo][eblo2]'; -> $_ref =& $_r[zhopa][zyu][xep][pezda][chmo][ololo][eblo];
			$ka = explode( '[', str_replace( ']', '', $k ) );
			$_ref =& $_r;
			for( $i = 0; $i < count($ka); $i++ )
			if( isset( $ka[$i] ) ) {
				if( !isset( $_ref[$ka[$i]] ) || !is_array( $_ref[$ka[$i]] ) ) $_ref[$ka[$i]] = array( );
				if( !isset( $ka[$i+1] ) ) break; // if last exists
				$_ref =& $_ref[$ka[$i]];
			}
			if( $ka[$i] == '' )	$_ref[] = $v;
			else				$_ref[$ka[$i]] = $v;
			
			if( isset( $f['oneof'] ) && $f['oneof'] && $v )
				$oneof[$f['oneof']] = $v;
		}
		// data fetched.

			// back multipliying
			// - not needed
			
			// back typifying
			if( $f['type'] == 'typeable' && strpos( $fieldKey, '{type}' ) )
			{
				// go to {type} contains array
				$ka = explode( '[', str_replace( ']', '', substr( $fieldKey, 0, strpos( $fieldKey, '[{type}]' ) ) ) );
				$_ref =& $_r; for( $i = 0; $i < count($ka); $i++ ) $_ref =& $_ref[$ka[$i]];

				// fetch data and remove typeable fields without type
				$fieldData = $_ref['{type}'];
				unset( $_ref['{type}'] );
				
				if( is_array( $fieldData['type'] ) )
				foreach( $fieldData['type'] as $k => $type )
				if( isset( $fieldData['value'][$k] )
					&& array_key_exists( $type, $f['types']) )
				{
					$value = $fieldData['value'][$k];
					$keyPath = explode( '[', str_replace( ']', '', str_replace( '{type}', $type, $fieldKey ) ) );
					//fb::log( $keyPath, 'path '.str_replace( array( ']', '[]' ), '', str_replace( '{type}', $type, $fieldKey ) ) );
					$_ref =& $_r;
					for( $i = 0; $i < count($keyPath) && isset( $keyPath[$i] ); $i++ )
					{
						$key = $keyPath[$i];
						if( $key == '' ) break;
						if( !isset( $_ref[$key] ) || !is_array( $_ref[$key] ) ) $_ref[$key] = array( );
						$_ref =& $_ref[$key];
					}
					if( $key == '' ) $_ref[] = $value; else $_ref[$key] = $value;
				}
			}
		}


		//fb::info( $_r['fields'], '$[fields]' );

		foreach( $oneof as $group => $value )
		if( is_null( $value ) || !$value )
			throw new FormException( sprintf( "Одно из полей %s должно быть заполнено.", $group ), 0, null, $k );
		
		return $_r;
	}
	
	
	public function getFormData( )
	{
		$r = array( );
		$r['fields'] = $this ->getFieldSet( );
		$r['options'] = $this ->getOptions( );
		
		return $r;
	}
	
}

